package com.ccx.demo.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.ccx.demo.business.common.dto.TabErrorLogInsertDTO;
import com.ccx.demo.business.common.service.ErrorLogService;
import com.ccx.demo.config.init.AppConfig;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.support.mvc.entity.IUser;
import com.support.mvc.entity.base.Result;
import com.utils.enums.Code;
import com.utils.exception.CodeException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.BindException;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.validation.ValidationException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Charsets.UTF_8;

/**
 * <pre>
 * Spring Core，参考：
 * https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html
 *
 * Spring MVC 配置，参考 ：
 * https://linesh.gitbooks.io/spring-mvc-documentation-linesh-translation/content/
 * https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html
 *
 * Spring Thymeleaf 配置，参考 ：
 * https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#spring-mvc-configuration
 *
 * Spring validator 配置，参考
 * https://beanvalidation.org/2.0/spec/#introduction
 * https://www.baeldung.com/javax-validation-method-constraints
 * http://rubygems.torquebox.org/proposals/BVAL-241/
 * https://juejin.im/entry/5b5a94d2f265da0f7c4fd2b2
 * https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
 *
 * @author 谢长春 2018-10-3
 */
@RestControllerAdvice
@Slf4j
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
//@EnableWebMvc
public class AppConfiguration {
    private final AppConfig appConfig;

    /**
     * 静态文件映射配置
     */
    @Configuration(proxyBeanMethods = false)
    public static class StaticAssetConfiguration implements WebMvcConfigurer {
        @Autowired
        private AppConfig appConfig;

        @Override
        public void addResourceHandlers(final ResourceHandlerRegistry registry) {
            // 添加静态资源过滤
            registry.addResourceHandler("favicon.ico")
                    .addResourceLocations("classpath:/static/")
                    .setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic());
            // 需要在 Spring Security 中配置忽略静态资源 WebSecurity.ignoring().antMatchers("/static/**");
            registry.addResourceHandler("/static/**")
                    // Locations 这里应该是编译后的静态文件目录
                    .addResourceLocations("classpath:/static/")
                    .setCacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic());

            // knife4j 增强 swagger 配置 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
            registry.addResourceHandler("doc.html")
                    .addResourceLocations("classpath:/META-INF/resources/")
                    .setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic());
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/")
                    .setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic());
            // knife4j 增强 swagger 配置 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

            // 添加静态资源过滤
            // 需要在 Spring Security 中配置忽略静态资源 WebSecurity.ignoring().antMatchers("/files/**");
            if (StringUtils.isNoneBlank(appConfig.getFiles().getDirectory())) {
                registry.addResourceHandler(String.format("/%s/**", appConfig.getName()))
                        // TODO 采坑记录结尾带 / 和不带 / 的区别
                        //   假设请求url为：http://{{host}}:{{port}}/files/temp/a.txt
                        //   addResourceLocations 指定绝对路径
                        //   d:/files => d:/temp/a.txt
                        //   d:/files/ => d:/files/temp/a.txt
                        .addResourceLocations(String.format("file:%s/", appConfig.getFiles().getDirectory()))
                ;
            }
        }

//    /**
//     * <pre>
//     * // http 查询参数构造 final String jsonText = "{\"key\": \"value\", \"number\": 1, \"date\": \"2020-01-01\", \"datetime\": \"2020-01-01 01:01:01\", \"rangeInt\": {\"min\": 1, \"max\": 100}, \"rangeDouble\": {\"min\": \"1.1\", \"max\": \"100.1\"}, \"dateRange\": {\"begin\": \"2020-01-01\", \"end\": \"2020-12-01\"}, \"ids\": [1, 2, 3, 4],, \"names\": [\"a\",\"b\",\"c\",\"d\"], \"orderBy\": [{\"name\": \"id\", \"direction\": \"DESC\"}, {\"name\": \"updateTime\", \"direction\": \"DESC\"}]}";
//     * final JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(queryParams));
//     * if (!jsonObject.isEmpty()) {
//     *     queryString = jsonObject.keySet().stream()
//     *             .map((key) -> {
//     *                 final Object value = jsonObject.get(key);
//     *                 if (value instanceof JSONArray) {
//     *                     JSONArray arr = (JSONArray) value;
//     *                     if (arr.isEmpty()) {
//     *                         return null;
//     *                     }
//     *                     if (arr.get(0) instanceof JSONObject) {
//     *                         return String.format("%s=%s", key, JSON.toJSONString(value));
//     *                     }
//     *                     return String.format("%s=%s", key, arr.stream().map(Objects::toString).collect(Collectors.joining(",")));
//     *                 } else if (value instanceof JSONObject) {
//     *                     if (((JSONObject) value).isEmpty()) {
//     *                         return null;
//     *                     }
//     *                     return String.format("%s=%s", key, JSON.toJSONString(value));
//     *                 } else {
//     *                     return String.format("%s=%s", key, value.toString());
//     *                 }
//     *             })
//     *             .filter(Objects::nonNull)
//     *             .collect(Collectors.joining("&"));
//     *     if (StringUtils.isNotBlank(queryString)) {
//     *         queryString = "?".concat(queryString);
//     *     }
//     * }
//     * <pre/>
//     */
//    @Override
//    public void addFormatters(FormatterRegistry registry) {
////        registry.addFormatter(new VarietyFormatter()); // 而VarietyFormatter可以自动转换我们的各种实体，将他们用在表单上
//        registry.addConverter(String.class, Date.class, value -> yyyy_MM_dd_HH_mm_ss_SSS.parseOfNullable(value).map(Dates::date).orElse(null));
//        registry.addConverter(String.class, Timestamp.class, value -> yyyy_MM_dd_HH_mm_ss_SSS.parseOfNullable(value).map(Dates::timestamp).orElse(null));
//        registry.addConverter(String.class, RangeInt.class, value -> JSON.parseObject(value, RangeInt.class));
//        registry.addConverter(String.class, RangeLong.class, value -> JSON.parseObject(value, RangeLong.class));
//
//        registry.addConverter(String.class, Dates.Range.class, value -> JSON.parseObject(value, Dates.Range.class));
//        registry.addConverter(new Converter<String, Range<BigDecimal>>() {
//            @Override
//            public Range<BigDecimal> convert(final String value) {
//                return JSON.parseObject(value, new TypeReference<Range<BigDecimal>>() {
//                });
//            }
//        });
//        registry.addConverter(new Converter<String, Range<Double>>() {
//            @Override
//            public Range<Double> convert(final String value) {
//                return JSON.parseObject(value, new TypeReference<Range<Double>>() {
//                });
//            }
//        });
//        registry.addConverter(new Converter<String, List<OrderBy>>() {
//            @Override
//            public List<OrderBy> convert(final String value) {
//                return JSON.parseArray(value, OrderBy.class);
//            }
//        });
////        registry.addConverter(new Converter<String, Object[]>() {
////            @Override
////            public Object[] convert(final String dataString) {
////                log.info(dataString);
//////                return yyyy_MM_dd_HH_mm_ss_SSS.parseOfNullable(dateString).map(Dates::timestamp).orElse(null);
////                return null;
////            }
////        });
//    }

//    /**
//     * 支持异步响应配置，参考：https://linesh.gitbooks.io/spring-mvc-documentation-linesh-translation/content/publish/21-3/4-asynchronous-request-processing.html
//     */
//    @Override
//    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
//    }
    }

//    /**
//     * 服务端 500 异常处理
//     * 需要自定义 Controller 继承 {@link AbstractMvcConfig.ErrorController}
//     * spring security 需要添加 http.antMatchers("/error").permitAll()
//     *
//     * @author 谢长春
//     */
//    public static class ErrorController extends AbstractErrorController {
//        public ErrorController(ErrorAttributes errorAttributes) {
//            super(errorAttributes);
//        }
//
//        @Override
//        public String getErrorPath() {
//            return "/error";
//        }
//
//        /**
//         * 处理服务端 500 异常
//         */
//        @RequestMapping(value = "/error", method = {GET, POST, PUT, PATCH, DELETE})
//        @ResponseBody
//        public Result<Void> error() {
//            return Code.A00001.toResult("500：请求失败，不明确的异常");
//        }
//    }

    /**
     * 多线程管理
     *
     * @return ExecutorService
     */
    @Bean(destroyMethod = "shutdownNow")
    public ExecutorService multiThread() {
        return new ThreadPoolExecutor(
                16,
                16 * 4,
                1L,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(4096), // 线程池队列，超过 4096 个任务将会抛出异常
                new ThreadFactoryBuilder().setNameFormat("multi-thread-%d").build(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }

    /**
     * 单线程管理
     *
     * @return ExecutorService
     */
    @Bean(destroyMethod = "shutdownNow")
    public ExecutorService singleThread() {
        return new ThreadPoolExecutor(
                1,
                1,
                1L,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(4096), // 线程池队列，超过 4096 个任务将会抛出异常
                new ThreadFactoryBuilder().setNameFormat("single-thread-%d").build(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }

    /**
     * 启用 FastJson
     */
    @Bean
    public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() {
        JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.DisableCircularReferenceDetect.getMask(); // 解决循环引用问题
        final FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        converter.setDefaultCharset(UTF_8);
        converter.getFastJsonConfig().setFeatures(Feature.OrderedField);
        if (Objects.equals("prod", appConfig.getEnv())) {  // 生产环境不返回 Result#exception
            final SimplePropertyPreFilter simplePropertyPreFilter = new SimplePropertyPreFilter();
            simplePropertyPreFilter.getExcludes().add("exception");
            converter.getFastJsonConfig().setSerializeFilters(simplePropertyPreFilter);
        }
        converter.setSupportedMediaTypes(Collections.singletonList(
                MediaType.APPLICATION_JSON
        ));
        return converter;
    }

    /**
     * Spring validator 方法级别的校验
     * https://www.baeldung.com/javax-validation-method-constraints
     * https://juejin.im/entry/5b5a94d2f265da0f7c4fd2b2
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
    /*
     * 快速失败返回模式，只要有一个异常就返回
     * https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
     @Bean
     public Validator validator() {
     return Validation.byProvider(HibernateValidator.class)
     .configure()
     .failFast(true)
     .addProperty( "hibernate.validator.fail_fast", "true" )
     .buildValidatorFactory()
     .getValidator();
     }
     */

//    /**
//     * 注册过滤器
//     * <pre>
//     * 添加自定义过滤器：设置链路追踪，在日志打印和响应头中追加该标记
//     * 警告：多线程时需要特殊处理
//     * final Map<String, String> mdc = MDC.getCopyOfContextMap(); // 复制主线程 ThreadLocal
//     * new Thread(() -> {
//     *     try {
//     *         MDC.setContextMap(mdc); // 设置子线程 ThreadLocal
//     *         // 子线程代码
//     *     } finally {
//     *         MDC.clear(); // 清除子线程 ThreadLocal
//     *     }
//     * }).start();
//     *
//     * @return FilterRegistrationBean
//     */
//    @Order(0)
//    @Bean
//    public FilterRegistrationBean<Filter> requestIdFilter() {
//        final FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
//        bean.setOrder(0);
//        bean.setFilter(new RequestIdFilter());
//        bean.addUrlPatterns("/*");
//        return bean;
//    }

//    @ExceptionHandler
//    public Result<Void> exception(final JSONException e) {
//        return new Result<Void>(Code.A00008).setException(String.format("500：JSON 序列化或反序列化异常：%s", e.getMessage()));
//    }

    /**
     * 解决 springboot 2.6.x 与 springfox 兼容性问题
     * https://github.com/springfox/springfox/issues/3791
     * https://github.com/springfox/springfox/issues/3462
     *
     * @param webEndpointsSupplier
     * @param servletEndpointsSupplier
     * @param controllerEndpointsSupplier
     * @param endpointMediaTypes
     * @param corsProperties
     * @param webEndpointProperties
     * @param environment
     * @return
     */
    @Bean
    public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(
            WebEndpointsSupplier webEndpointsSupplier,
            ServletEndpointsSupplier servletEndpointsSupplier,
            ControllerEndpointsSupplier controllerEndpointsSupplier,
            EndpointMediaTypes endpointMediaTypes,
            CorsEndpointProperties corsProperties,
            WebEndpointProperties webEndpointProperties,
            Environment environment) {
        final List<ExposableEndpoint<?>> allEndpoints = Lists.newArrayList();
        final Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
        allEndpoints.addAll(webEndpoints);
        allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
        allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
        final String basePath = webEndpointProperties.getBasePath();
        final EndpointMapping endpointMapping = new EndpointMapping(basePath);
        boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
        return new WebMvcEndpointHandlerMapping(
                endpointMapping,
                webEndpoints,
                endpointMediaTypes,
                corsProperties.toCorsConfiguration(),
                new EndpointLinksResolver(allEndpoints, basePath),
                shouldRegisterLinksMapping,
                null
        );
    }

    private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
        return webEndpointProperties.getDiscovery().isEnabled()
                && (org.springframework.util.StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
    }

    @Autowired
    @Lazy
    private ErrorLogService errorLogService;

    @ExceptionHandler
    public Result<Void> exception(final Exception e) {
        log.error("{}"
                , Optional
                        .ofNullable(SecurityContextHolder.getContext().getAuthentication())
                        .map(Authentication::getPrincipal)
                        .map(obj -> (IUser) obj)
                        .map(obj -> {
                            final String msg = String.format("%s:%d:%s:%s"
                                    , (e instanceof CodeException) ? ((CodeException) e).getCode() : "-"
                                    , obj.getId()
                                    , obj.getUsername()
                                    , obj.getPhone()
                            );
                            // 写入 trace_log
                            final TabErrorLogInsertDTO error = new TabErrorLogInsertDTO();
                            error.setTraceId(MDC.get("traceId"));
                            error.setMessage(Strings.left(msg + ":" + e.getMessage(), 260000));
                            errorLogService.insert(error, Optional.ofNullable(obj.getId()).orElse(0L));
                            return msg;
                        })
                        .orElse("")
                , e
        );
        if (e instanceof CodeException) {
            return new Result<Void>().setException(e);
        } else if (e instanceof BadCredentialsException) {
            return new Result<Void>(Code.A00006).setException("401：无操作权限");
        } else if (e instanceof AccessDeniedException) {
            return new Result<Void>(Code.A00006).setException("403：无操作权限");
        } else if (e instanceof ValidationException) {
            return new Result<Void>(Code.A00003).setException(e.getMessage());
        } else if (e instanceof HttpMediaTypeNotSupportedException) {
            return new Result<Void>(Code.A00003).setException(String.format("500：content-type 不支持：%s", e.getMessage()));
        } else if (e instanceof MaxUploadSizeExceededException) {
            return new Result<Void>(Code.A00005).setException("文件大小超过限制");
        } else if (e instanceof IllegalArgumentException) {
            return new Result<Void>(Code.A00003).setException("400：请求不存在");
        } else if (e instanceof MissingServletRequestParameterException) {
            return new Result<Void>(Code.A00003).setException(String.format("500：请求 url 映射的方法缺少必要的参数：%s", e.getMessage()));
        } else if (e instanceof HttpMessageNotReadableException) {
            return new Result<Void>(Code.A00003).setException(String.format("500：请求参数解析失败:%s", e.getMessage()));
        } else if (e instanceof JSONException) {
            return new Result<Void>(Code.A00003).setException(String.format("500：JSON 序列化或反序列化异常：%s", e.getMessage()));
        } else if (e instanceof BindException || e instanceof MethodArgumentTypeMismatchException) {
            return new Result<Void>(Code.A00003).setException(String.format("500：参数转换异常：%s", e.getMessage()));
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
            return new Result<Void>(Code.A00003).setException(String.format("405：请求方式不被该接口支持，或者请求url错误未映射到正确的方法：%s", e.getMessage()));
        } else if (e instanceof HttpMessageNotWritableException) {
            return new Result<Void>(Code.A00003).setException(String.format("500：请求缺少必要的参数:%s", e.getMessage()));
        } else if (e instanceof NoHandlerFoundException) {
            return new Result<Void>(Code.A00003).setException("404：请求url不存在");
        }
        return new Result<Void>(Code.A00003).setException(String.format("500：请求失败，不明确的异常：%s", e.getMessage()));
    }

}
